package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.gmall.list.model.*;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.product.client.ProductFeignClient;
import com.atguigu.gmall.product.model.BaseAttrInfo;
import com.atguigu.gmall.product.model.BaseCategoryView;
import com.atguigu.gmall.product.model.BaseTrademark;
import com.atguigu.gmall.product.model.SkuInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.Operator;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

/**
 * @author: atguigu
 * @create: 2023-09-09 09:12
 */
@Slf4j
@Service
public class SearchServiceImpl implements SearchService {


    private static final String INDEX_NAME = "goods";


    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 商品上架，需要构建索引库文档对象；将文档存入索引库
     *
     * @param skuId
     */
    @Override
    public void upperGoods(Long skuId) {
        try {
            //1.构建索引库文档对象Goods-远程调用商品服务多个接口，采用异步+线程池进行并行处理
            Goods goods = new Goods();
            //1.1 封装商品基本信息
            CompletableFuture<SkuInfo> skuInfoCompletableFuture = CompletableFuture.supplyAsync(() -> {
                SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);
                if (skuInfo == null) {
                    throw new RuntimeException("商品不存在！");
                }
                goods.setId(skuId);
                goods.setDefaultImg(skuInfo.getSkuDefaultImg());
                goods.setTitle(skuInfo.getSkuName());
                goods.setCreatedDate(skuInfo.getCreateTime());
                goods.setCreateTime(skuInfo.getCreateTime());
                return skuInfo;
            }, threadPoolExecutor);
            //1.2 封装商品价格
            CompletableFuture<Void> priceCompletableFuture = CompletableFuture.runAsync(() -> {
                BigDecimal skuPrice = productFeignClient.getSkuPrice(skuId);
                goods.setPrice(skuPrice.doubleValue());
            }, threadPoolExecutor);

            //1.3 封装商品品牌信息
            CompletableFuture<Void> trademarkCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
                if (trademark != null) {
                    goods.setTmId(trademark.getId());
                    goods.setTmName(trademark.getTmName());
                    goods.setTmLogoUrl(trademark.getLogoUrl());
                }
            }, threadPoolExecutor);


            //1.4 封装商品分类信息
            CompletableFuture<Void> categoryCompletableFuture = skuInfoCompletableFuture.thenAcceptAsync(skuInfo -> {
                BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
                if (categoryView != null) {
                    goods.setCategory1Id(categoryView.getCategory1Id());
                    goods.setCategory1Name(categoryView.getCategory1Name());
                    goods.setCategory2Id(categoryView.getCategory2Id());
                    goods.setCategory2Name(categoryView.getCategory2Name());
                    goods.setCategory3Id(categoryView.getCategory3Id());
                    goods.setCategory3Name(categoryView.getCategory3Name());
                }
            }, threadPoolExecutor);

            //1.5 封装商品平台属性列表
            CompletableFuture<Void> attrListCompletableFuture = CompletableFuture.runAsync(() -> {
                List<BaseAttrInfo> attrInfoList = productFeignClient.getAttrListBySkuId(skuId);
                if (!CollectionUtils.isEmpty(attrInfoList)) {
                    //将集合泛型从BaseAttrInfo转为SearchAttr
                    List<SearchAttr> searchAttrList = attrInfoList.stream().map(baseAttrInfo -> {
                        SearchAttr searchAttr = new SearchAttr();
                        searchAttr.setAttrId(baseAttrInfo.getId());
                        searchAttr.setAttrName(baseAttrInfo.getAttrName());
                        searchAttr.setAttrValue(baseAttrInfo.getAttrValue());
                        return searchAttr;
                    }).collect(Collectors.toList());
                    goods.setAttrs(searchAttrList);
                }
            }, threadPoolExecutor);

            CompletableFuture.allOf(
                    skuInfoCompletableFuture,
                    priceCompletableFuture,
                    categoryCompletableFuture,
                    trademarkCompletableFuture,
                    attrListCompletableFuture
            ).join();

            //2.调用ES提供JavaClient将文档存入索引库
            //2.1 构建新增文档请求对象
            IndexRequest indexRequest = new IndexRequest(INDEX_NAME);
            //2.1 设置文档ID
            indexRequest.id(skuId.toString());
            //2.3 设置文档请求体参数 JSON
            String goodJsonStr = JSON.toJSONString(goods);
            indexRequest.source(goodJsonStr, XContentType.JSON);

            //3.执行新增文档请求
            restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        } catch (Exception e) {
            throw new RuntimeException("文档新增失败：{}", e);
        }
    }

    /**
     * 商品下架，将商品文档从索引库删除
     *
     * @param skuId
     */
    @Override
    public void lowerGoods(Long skuId) {
        try {
            //1.构建删除请求对象
            DeleteRequest deleteRequest = new DeleteRequest(
                    INDEX_NAME,
                    skuId.toString()
            );
            //2.执行删除文档
            restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("[搜索服务]删除文档失败：文档ID，{}，{}\", skuId.toString(), e.getMessage()");
            throw new RuntimeException("删除文档失败");
        }
    }

    /**
     * 对指定商品热门分值进行增加/减少
     *
     * @param skuId
     * @param incrscore
     */
    @Override
    public void incrHotScore(String skuId, int incrscore) {
        try {
            //1.先修改Redis中商品热门分值
            String hotKey = "hot:goods:score";
            Double goodsScore = redisTemplate.opsForZSet().incrementScore(hotKey, skuId, incrscore);

            //2.再满足写ES条件后再对ES进行更新-稀释写操作
            if (goodsScore % 10 == 0) {
                UpdateRequest updateRequest = new UpdateRequest(
                        INDEX_NAME,
                        skuId);
                Goods goods = new Goods();
                goods.setHotScore(goodsScore.longValue());
                updateRequest.doc(JSON.toJSONString(goods), XContentType.JSON);
                restHighLevelClient.update(
                        updateRequest, RequestOptions.DEFAULT);
            }
        } catch (IOException e) {
            log.error("[搜索服务]更新热门分值异常：商品ID{}，异常信息：{}", skuId, e);
            throw new RuntimeException("更新热门分值异常");
        }
    }

    /**
     * 站内商品检索接口。
     * 实现检索三大开发步骤：
     * 1.准备用于检索请求对象
     * 2.执行检索
     * 3.解析ES响应结果
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo search(SearchParam searchParam) {
        try {
            //一、基于入参查询条件构建出用于搜索请求对象SearchRequest 涵盖：检索请求路径以及请求体参数
            SearchRequest searchRequest = this.buildDSL(searchParam);

            System.out.println("本次检索DSL:");
            System.err.println(searchRequest.source().toString());

            //二、调用ES提供检索接口-本质发起http请求
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

            //三、按照接口文档要求进行解析结果
            return this.parseResult(response, searchParam);
        } catch (Exception e) {
            log.error("[搜索服务]检索异常：检索条件：{}，异常信息：{}", searchParam.toString(), e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 基于入参查询条件，封装DSL语句相关请求路径以及请求体参数
     *
     * @param searchParam
     * @return
     */
    @Override
    public SearchRequest buildDSL(SearchParam searchParam) {
        //1.构建用于检索请求对象-封装检索目标索引库名称 产生http请求路径: GET goods/_search
        SearchRequest searchRequest = new SearchRequest(INDEX_NAME);

        //2.构建用户封装请求体参数对象-封装各种请求体参数包括：1.query 2.from size 3.highlight 4.sort 5._source 6.aggs
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //2.1 设置用于查询过滤数据条件 "query"部分 包含：关键字；品牌ID；分类ID；平台属性过滤
        //2.1.1 创建用于封装多个查询条件查询构建器对象-采用bool查询
        BoolQueryBuilder allBoolQuery = QueryBuilders.boolQuery();
        //2.1.2 设置布尔查询中必须满足条件：关键字分词查询
        String keyword = searchParam.getKeyword();
        if (StringUtils.isNotBlank(keyword)) {
            allBoolQuery.must(QueryBuilders.matchQuery("title", keyword).operator(Operator.AND));
        }
        //2.1.3 设置布尔查询中过滤条件：①品牌ID过滤 ②分类ID过滤 ③平台属性过滤
        //2.1.3.1 如有品牌ID过滤条件则设置过滤 参数： trademark=5:华为
        String trademark = searchParam.getTrademark();
        if (StringUtils.isNotBlank(trademark)) {
            String[] split = trademark.split(":");
            if (split != null && split.length == 2) {
                allBoolQuery.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }
        //2.1.3.2 如有三个分类ID过滤条件则设置过滤
        Long category1Id = searchParam.getCategory1Id();
        if (category1Id != null) {
            allBoolQuery.filter(QueryBuilders.termQuery("category1Id", category1Id));
        }
        Long category2Id = searchParam.getCategory2Id();
        if (category2Id != null) {
            allBoolQuery.filter(QueryBuilders.termQuery("category2Id", category2Id));
        }
        Long category3Id = searchParam.getCategory3Id();
        if (category3Id != null) {
            allBoolQuery.filter(QueryBuilders.termQuery("category3Id", category3Id));
        }
        //2.1.3.3 如有平台属性过滤条件则设置过滤
        //2.1.3.3.1 获取前端提交多组平台属性过滤条件 每组过滤条件形式：平台属性Id:平台属性值名称:平台属性名
        String[] props = searchParam.getProps();
        if (props != null && props.length > 0) {
            //2.1.3.3.2 循环处理每一组平台属性过滤条件
            for (String prop : props) {
                //每个平台属性过滤条件形式：23:8G:运行内存
                String[] split = prop.split(":");
                if (split != null && split.length == 3) {
                    //2.1.3.3.3 每个Nested查询都采用bool查询封装两个条件
                    BoolQueryBuilder attrBoolQueryBuilder = QueryBuilders.boolQuery();
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrId", split[0]));
                    attrBoolQueryBuilder.must(QueryBuilders.termQuery("attrs.attrValue", split[1]));
                    //2.1.3.3.4 每循环一次构建改组过滤条件Nested查询
                    //参数1：Nested类型字段名称 参数二：Nested中包含查询条件 参数三：文档得分模式设置
                    NestedQueryBuilder attrsNestedQueryBuilder = QueryBuilders.nestedQuery("attrs", attrBoolQueryBuilder, ScoreMode.None);
                    allBoolQuery.filter(attrsNestedQueryBuilder);
                }
            }
        }
        searchSourceBuilder.query(allBoolQuery);
        //2.2 设置分页 "from" "size" 其中from计算=（页码-1）*页大小 包含：起始文档索引 页大小
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        int from = (pageNo - 1) * pageSize;
        searchSourceBuilder.from(from).size(pageSize);

        //2.3 设置高亮 "highlight" 前提：关键字有值 包含：高亮字段；html标签（前置，后置）
        if (StringUtils.isNotBlank(keyword)) {
            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("title");
            highlightBuilder.preTags("<font style='color:red'>");
            highlightBuilder.postTags("</font>");
            searchSourceBuilder.highlighter(highlightBuilder);
        }

        //2.4 设置排序 "sort" 前提：默认排序相关度得分，需要排序  包含：排序字段 排序方式
        //2.4.1 判断前端是否提交排序字段 参数形式：order=排序字段:升序|降序=1:asc   排序字段：1热度 2价格
        String order = searchParam.getOrder();
        if (StringUtils.isNotBlank(order)) {
            //2.4.2 声明排序字段默认""
            String orderFiled = "";
            String[] split = order.split(":");
            if (split != null && split.length == 2) {
                if ("1".equals(split[0])) {
                    orderFiled = "hotScore";
                }
                if ("2".equals(split[0])) {
                    orderFiled = "price";
                }
                searchSourceBuilder.sort(orderFiled, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);
            }
        }

        //2.5 设置响应字段 "_source" 包含：需要响应业务字段，满足展示业务数据（商品SKU）即可
        searchSourceBuilder.fetchSource(new String[]{"id", "title", "price", "defaultImg"}, null);

        //2.6 设置聚合 "aggs" 包含：需要动态用于展示过滤条件：1.品牌 2.平台属性 三要素：①聚合名称 ②聚合字段  ③聚合类型
        //2.6.1 设置品牌聚合
        //2.6.1.1 构建品牌ID聚合对象，根据品牌ID进行聚合 terms方法：聚合类型  terms("聚合名称").field("聚合字段")
        TermsAggregationBuilder tmIdAgg = AggregationBuilders.terms("tmIdAgg").field("tmId");
        //2.6.1.2 基于品牌ID聚合对象构建品牌名称子聚合，根据品牌名称聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"));
        //2.6.1.3 基于品牌ID聚合对象构建品牌Logo子聚合，根据品牌Logo聚合
        tmIdAgg.subAggregation(AggregationBuilders.terms("tmLogoAgg").field("tmLogoUrl"));
        //2.6.1.4 设置品牌ID聚合
        searchSourceBuilder.aggregation(tmIdAgg);

        //2.6.2 设置平台属性聚合
        //2.6.2.1 构建平台属性聚合对象（nested聚合-字段attrs）
        NestedAggregationBuilder nestedAggregation = AggregationBuilders.nested("attrsAgg", "attrs");

        //2.6.2.2 基于平台属性聚合对象构建平台属性ID子聚合-根据平台属性id聚合
        TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrIdAgg").field("attrs.attrId");

        //2.6.2.3 基于平台属性ID聚合对象，构建平台属性名称子聚合-根据平台属性名称聚合
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName"));
        //2.6.2.4 基于平台属性ID聚合对象，构建平台属性值子聚合-根据平台属性值聚合
        attrIdAgg.subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue"));

        nestedAggregation.subAggregation(attrIdAgg);

        //2.6.2.4 将平台属性聚合放入请求体参数
        searchSourceBuilder.aggregation(nestedAggregation);
        //3.将请求体参数放入到请求对象中
        return searchRequest.source(searchSourceBuilder);
    }


    /**
     * 解析ES响应结果 按照接口文档封装vo对象
     *
     * @param response
     * @param searchParam
     * @return
     */
    @Override
    public SearchResponseVo parseResult(SearchResponse response, SearchParam searchParam) {
        SearchResponseVo vo = new SearchResponseVo();
        //1.封装结果vo中分页信息
        Integer pageNo = searchParam.getPageNo();
        Integer pageSize = searchParam.getPageSize();
        vo.setPageSize(pageSize);
        vo.setPageNo(pageNo);
        //获取ES响应结果中总记录数
        long total = response.getHits().getTotalHits().value;
        vo.setTotal(total);
        //计算总页数 判断总记录数对页大小进行整除
        long totalPage = (total % pageSize == 0) ? total / pageSize : total / pageSize + 1;
        vo.setTotalPages(totalPage);

        //2.封装结果vo中当前页商品集合
        SearchHit[] hits = response.getHits().getHits();
        if (hits != null && hits.length > 0) {
            List<SearchHit> searchHits = Arrays.asList(hits);
            List<Goods> goodsList = searchHits.stream().map(searchHit -> {
                String goodsStr = searchHit.getSourceAsString();
                Goods goods = JSON.parseObject(goodsStr, Goods.class);
                //处理高亮片段
                Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                if (!CollectionUtils.isEmpty(highlightFields)) {
                    HighlightField highlightField = highlightFields.get("title");
                    Text highlightFieldFragment = highlightField.getFragments()[0];
                    goods.setTitle(highlightFieldFragment.toString());
                }
                return goods;
            }).collect(Collectors.toList());
            vo.setGoodsList(goodsList);
        }
        //3.封装结果vo中聚合品牌列表
        //3.1 先获取聚合结果对象-封装(品牌，平台属性)聚合结果转为Map
        Map<String, Aggregation> allAggregationMap = response.getAggregations().asMap();
        //3.1.1 处理品牌ID聚合结果-循环获取品牌ID聚合结果
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) allAggregationMap.get("tmIdAgg");
        if (tmIdAgg != null && !CollectionUtils.isEmpty(tmIdAgg.getBuckets())) {
            List<SearchResponseTmVo> tmVoList = tmIdAgg.getBuckets().stream().map(bucket -> {
                //获取到聚合到品牌ID
                long tmId = bucket.getKeyAsNumber().longValue();
                //3.1.2 在品牌ID聚合中获取品牌名称聚合对象-获取品牌名称
                ParsedStringTerms tmNameAgg = bucket.getAggregations().get("tmNameAgg");
                String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
                //3.1.3 在品牌ID聚合中获取品牌Logo聚合对象-获取品牌Logo
                ParsedStringTerms tmLogoAgg = bucket.getAggregations().get("tmLogoAgg");
                String tmLogoUrl = tmLogoAgg.getBuckets().get(0).getKeyAsString();
                //3.1.4 创建聚合到品牌VO对象
                SearchResponseTmVo tmVo = new SearchResponseTmVo();
                tmVo.setTmId(tmId);
                tmVo.setTmName(tmName);
                tmVo.setTmLogoUrl(tmLogoUrl);
                return tmVo;
            }).collect(Collectors.toList());
            vo.setTrademarkList(tmVoList);
        }
        //4.封装结果vo中聚合平台属性列表
        ParsedNested attrsAgg = (ParsedNested) allAggregationMap.get("attrsAgg");
        if (attrsAgg != null) {
            //4.1 基于平台属性聚合对象获取平台属性ID子聚合对象
            ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
            if (attrsAgg != null && !CollectionUtils.isEmpty(attrIdAgg.getBuckets())) {
                //4.2 处理平台属性ID子聚合对象-遍历聚合到平台属性ID桶
                List<SearchResponseAttrVo> attrVoList = attrIdAgg.getBuckets().stream().map(bucket -> {

                    //4.2.1 获取平台属性ID
                    long attrId = bucket.getKeyAsNumber().longValue();

                    //4.2.2 在当前平台属性ID聚合内部获取平台属性名称子聚合-获取平台属性名称（一个）
                    ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrNameAgg");
                    String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();

                    //4.2.3 在当前平台属性ID聚合内部获取平台属性值子聚合-获取平台值（不同文档具有不同平台属性值）
                    ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValueAgg");
                    List<String> attrValueList = attrValueAgg.getBuckets().stream()
                            .map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());

                    //4.2.4 构建平台属性聚合VO对象
                    SearchResponseAttrVo attrVo = new SearchResponseAttrVo();
                    attrVo.setAttrId(attrId);
                    attrVo.setAttrName(attrName);
                    attrVo.setAttrValueList(attrValueList);
                    return attrVo;
                }).collect(Collectors.toList());
                vo.setAttrsList(attrVoList);
            }
        }
        return vo;
    }
}
